# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt

"""Definition of an Argument class and transformers for various argument types.

An Argument instance represents a pylint option to be handled by an argparse.ArgumentParser
"""


import argparse
import pathlib
import re
import sys
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Optional,
    Pattern,
    Sequence,
    Tuple,
    Type,
    Union,
)

from pylint import interfaces
from pylint import utils as pylint_utils
from pylint.config.callback_actions import _CallbackAction
from pylint.config.deprecation_actions import _NewNamesAction, _OldNamesAction

if sys.version_info >= (3, 8):
    from typing import Literal
else:
    from typing_extensions import Literal


_ArgumentTypes = Union[
    str,
    int,
    float,
    bool,
    Pattern[str],
    Sequence[str],
    Sequence[Pattern[str]],
    Tuple[int, ...],
]
"""List of possible argument types."""


def _confidence_transformer(value: str) -> Sequence[str]:
    """Transforms a comma separated string of confidence values."""
    values = pylint_utils._check_csv(value)
    for confidence in values:
        if confidence not in interfaces.CONFIDENCE_LEVEL_NAMES:
            raise argparse.ArgumentTypeError(
                f"{value} should be in {*interfaces.CONFIDENCE_LEVEL_NAMES,}"
            )
    return values


def _csv_transformer(value: str) -> Sequence[str]:
    """Transforms a comma separated string."""
    return pylint_utils._check_csv(value)


YES_VALUES = {"y", "yes", "true"}
NO_VALUES = {"n", "no", "false"}


def _yn_transformer(value: str) -> bool:
    """Transforms a yes/no or stringified bool into a bool."""
    value = value.lower()
    if value in YES_VALUES:
        return True
    if value in NO_VALUES:
        return False
    raise argparse.ArgumentTypeError(
        None, f"Invalid yn value '{value}', should be in {*YES_VALUES, *NO_VALUES}"
    )


def _non_empty_string_transformer(value: str) -> str:
    """Check that a string is not empty and remove quotes."""
    if not value:
        raise argparse.ArgumentTypeError("Option cannot be an empty string.")
    return pylint_utils._unquote(value)


def _py_version_transformer(value: str) -> Tuple[int, ...]:
    """Transforms a version string into a version tuple."""
    try:
        version = tuple(int(val) for val in value.replace(",", ".").split("."))
    except ValueError:
        raise argparse.ArgumentTypeError(
            f"{value} has an invalid format, should be a version string. E.g., '3.8'"
        ) from None
    return version


def _regexp_csv_transfomer(value: str) -> Sequence[Pattern[str]]:
    """Transforms a comma separated list of regular expressions."""
    patterns: List[Pattern[str]] = []
    for pattern in _csv_transformer(value):
        patterns.append(re.compile(pattern))
    return patterns


def _regexp_paths_csv_transfomer(value: str) -> Sequence[Pattern[str]]:
    """Transforms a comma separated list of regular expressions paths."""
    patterns: List[Pattern[str]] = []
    for pattern in _csv_transformer(value):
        patterns.append(
            re.compile(
                str(pathlib.PureWindowsPath(pattern)).replace("\\", "\\\\")
                + "|"
                + pathlib.PureWindowsPath(pattern).as_posix()
            )
        )
    return patterns


_TYPE_TRANSFORMERS: Dict[str, Callable[[str], _ArgumentTypes]] = {
    "choice": str,
    "csv": _csv_transformer,
    "float": float,
    "int": int,
    "confidence": _confidence_transformer,
    "non_empty_string": _non_empty_string_transformer,
    "py_version": _py_version_transformer,
    "regexp": re.compile,
    "regexp_csv": _regexp_csv_transfomer,
    "regexp_paths_csv": _regexp_paths_csv_transfomer,
    "string": pylint_utils._unquote,
    "yn": _yn_transformer,
}
"""Type transformers for all argument types.

A transformer should accept a string and return one of the supported
Argument types. It will only be called when parsing 1) command-line,
2) configuration files and 3) a string default value.
Non-string default values are assumed to be of the correct type.
"""


class _Argument:
    """Class representing an argument to be parsed by an argparse.ArgumentsParser.

    This is based on the parameters passed to argparse.ArgumentsParser.add_message.
    See:
    https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
    """

    def __init__(
        self,
        *,
        flags: List[str],
        arg_help: str,
        hide_help: bool,
        section: Optional[str],
    ) -> None:
        self.flags = flags
        """The name of the argument."""

        self.hide_help = hide_help
        """Whether to hide this argument in the help message."""

        # argparse uses % formatting on help strings, so a % needs to be escaped
        self.help = arg_help.replace("%", "%%")
        """The description of the argument."""

        if hide_help:
            self.help = argparse.SUPPRESS

        self.section = section
        """The section to add this argument to."""


class _BaseStoreArgument(_Argument):
    """Base class for store arguments to be parsed by an argparse.ArgumentsParser.

    This is based on the parameters passed to argparse.ArgumentsParser.add_message.
    See:
    https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
    """

    def __init__(
        self,
        *,
        flags: List[str],
        action: str,
        default: _ArgumentTypes,
        arg_help: str,
        hide_help: bool,
        section: Optional[str],
    ) -> None:
        super().__init__(
            flags=flags, arg_help=arg_help, hide_help=hide_help, section=section
        )

        self.action = action
        """The action to perform with the argument."""

        self.default = default
        """The default value of the argument."""


class _StoreArgument(_BaseStoreArgument):
    """Class representing a store argument to be parsed by an argparse.ArgumentsParser.

    This is based on the parameters passed to argparse.ArgumentsParser.add_message.
    See:
    https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
    """

    def __init__(
        self,
        *,
        flags: List[str],
        action: str,
        default: _ArgumentTypes,
        arg_type: str,
        choices: Optional[List[str]],
        arg_help: str,
        metavar: str,
        hide_help: bool,
        section: Optional[str],
    ) -> None:
        super().__init__(
            flags=flags,
            action=action,
            default=default,
            arg_help=arg_help,
            hide_help=hide_help,
            section=section,
        )

        self.type = _TYPE_TRANSFORMERS[arg_type]
        """A transformer function that returns a transformed type of the argument."""

        self.choices = choices
        """A list of possible choices for the argument.

        None if there are no restrictions.
        """

        self.metavar = metavar
        """The metavar of the argument.

        See:
        https://docs.python.org/3/library/argparse.html#metavar
        """


class _StoreTrueArgument(_BaseStoreArgument):
    """Class representing a 'store_true' argument to be parsed by an argparse.ArgumentsParser.

    This is based on the parameters passed to argparse.ArgumentsParser.add_message.
    See:
    https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
    """

    # pylint: disable-next=useless-super-delegation # We narrow down the type of action
    def __init__(
        self,
        *,
        flags: List[str],
        action: Literal["store_true"],
        default: _ArgumentTypes,
        arg_help: str,
        hide_help: bool,
        section: Optional[str],
    ) -> None:
        super().__init__(
            flags=flags,
            action=action,
            default=default,
            arg_help=arg_help,
            hide_help=hide_help,
            section=section,
        )


class _DeprecationArgument(_Argument):
    """Store arguments while also handling deprecation warnings for old and new names.

    This is based on the parameters passed to argparse.ArgumentsParser.add_message.
    See:
    https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
    """

    def __init__(
        self,
        *,
        flags: List[str],
        action: Type[argparse._StoreAction],
        default: _ArgumentTypes,
        arg_type: str,
        choices: Optional[List[str]],
        arg_help: str,
        metavar: str,
        hide_help: bool,
        section: Optional[str],
    ) -> None:
        super().__init__(
            flags=flags, arg_help=arg_help, hide_help=hide_help, section=section
        )

        self.action = action
        """The action to perform with the argument."""

        self.default = default
        """The default value of the argument."""

        self.type = _TYPE_TRANSFORMERS[arg_type]
        """A transformer function that returns a transformed type of the argument."""

        self.choices = choices
        """A list of possible choices for the argument.

        None if there are no restrictions.
        """

        self.metavar = metavar
        """The metavar of the argument.

        See:
        https://docs.python.org/3/library/argparse.html#metavar
        """


class _StoreOldNamesArgument(_DeprecationArgument):
    """Store arguments while also handling old names.

    This is based on the parameters passed to argparse.ArgumentsParser.add_message.
    See:
    https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
    """

    def __init__(
        self,
        *,
        flags: List[str],
        default: _ArgumentTypes,
        arg_type: str,
        choices: Optional[List[str]],
        arg_help: str,
        metavar: str,
        hide_help: bool,
        kwargs: Dict[str, Any],
        section: Optional[str],
    ) -> None:
        super().__init__(
            flags=flags,
            action=_OldNamesAction,
            default=default,
            arg_type=arg_type,
            choices=choices,
            arg_help=arg_help,
            metavar=metavar,
            hide_help=hide_help,
            section=section,
        )

        self.kwargs = kwargs
        """Any additional arguments passed to the action."""


class _StoreNewNamesArgument(_DeprecationArgument):
    """Store arguments while also emitting deprecation warnings.

    This is based on the parameters passed to argparse.ArgumentsParser.add_message.
    See:
    https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
    """

    def __init__(
        self,
        *,
        flags: List[str],
        default: _ArgumentTypes,
        arg_type: str,
        choices: Optional[List[str]],
        arg_help: str,
        metavar: str,
        hide_help: bool,
        kwargs: Dict[str, Any],
        section: Optional[str],
    ) -> None:
        super().__init__(
            flags=flags,
            action=_NewNamesAction,
            default=default,
            arg_type=arg_type,
            choices=choices,
            arg_help=arg_help,
            metavar=metavar,
            hide_help=hide_help,
            section=section,
        )

        self.kwargs = kwargs
        """Any additional arguments passed to the action."""


class _CallableArgument(_Argument):
    """Class representing an callable argument to be parsed by an argparse.ArgumentsParser.

    This is based on the parameters passed to argparse.ArgumentsParser.add_message.
    See:
    https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument
    """

    def __init__(
        self,
        *,
        flags: List[str],
        action: Type[_CallbackAction],
        arg_help: str,
        kwargs: Dict[str, Any],
        hide_help: bool,
        section: Optional[str],
    ) -> None:
        super().__init__(
            flags=flags, arg_help=arg_help, hide_help=hide_help, section=section
        )

        self.action = action
        """The action to perform with the argument."""

        self.kwargs = kwargs
        """Any additional arguments passed to the action."""
